Skip to content

[GPDAPIM-258] SDS access module#79

Open
Vox-Ben wants to merge 24 commits intomainfrom
feature/GPCAPIM-258_SDS_access_module
Open

[GPDAPIM-258] SDS access module#79
Vox-Ben wants to merge 24 commits intomainfrom
feature/GPCAPIM-258_SDS_access_module

Conversation

@Vox-Ben
Copy link
Contributor

@Vox-Ben Vox-Ben commented Feb 9, 2026

Description

Creates the SDS access module and integrates it with the controller.

Context

CDG requires the ability to obtain organisation ASIDs and endpoints from SDS. This provides that capability.

Type of changes

  • Refactoring (non-breaking change)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would change existing functionality)
  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have followed the code style of the project
  • I have added tests to cover my changes
  • I have updated the documentation accordingly
  • This PR is a result of pair or mob programming
  • Exceptions/Exclusions to coding standards (e.g. #noqa or #NOSONAR) are included within this Pull Request.

Sensitive Information Declaration

To ensure the utmost confidentiality and protect your and others privacy, we kindly ask you to NOT including PII (Personal Identifiable Information) / PID (Personal Identifiable Data) or any other sensitive data in this PR (Pull Request) and the codebase changes. We will remove any PR that do contain any sensitive information. We really appreciate your cooperation in this matter.

  • I confirm that neither PII/PID nor sensitive data are included in this PR and the codebase changes.

@Vox-Ben Vox-Ben requested a review from a team as a code owner February 9, 2026 09:50
@github-actions
Copy link

github-actions bot commented Feb 9, 2026

Trivy gate: no Critical/High vulnerabilities.

Trivy Image Scan Summary

Image: 900119715266.dkr.ecr.eu-west-2.amazonaws.com/whoami:feature-gpcapim-258-sds-access-module

Severity Count
CRITICAL 0
HIGH 0
MEDIUM 0
LOW 1
UNKNOWN 0
Findings (top 50)
Severity ID Package Installed Fixed Source
LOW CVE-2026-1703 pip 25.3 26.0 Python

@github-actions
Copy link

github-actions bot commented Feb 10, 2026

Trivy gate: no Critical/High issues.

Trivy IaC (Terraform) Summary

Severity Count
CRITICAL 0
HIGH 0
MEDIUM 0
LOW 0
UNKNOWN 0
Findings (top 50)
Severity ID Title File

Comment on lines +78 to +82
# URLs for different SDS environments
SANDBOX_URL = "https://sandbox.api.service.nhs.uk/spine-directory/FHIR/R4"
INT_URL = "https://int.api.service.nhs.uk/spine-directory/FHIR/R4"
DEP_UAT_URL = "https://dep.api.service.nhs.uk/spine-directory/FHIR/R4"
PROD_URL = "https://api.service.nhs.uk/spine-directory/FHIR/R4"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see that these might be useful long-term, but are they adding value at this point?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any benefit to removing them? There's no test overhead or anything, and it means as and when later on we want to add them in we don't have to go looking for the addresses or make code changes to be able to handle them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(And we'll want at least sandbox and int retained anyway, won't we?)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - happy to leave for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably the urls will be passed in as config/env variables. We won't ever handle this class attributes.

@Vox-Ben Vox-Ben force-pushed the feature/GPCAPIM-258_SDS_access_module branch from 11f8a33 to db8aa1c Compare February 13, 2026 12:32
DWolfsNHS
DWolfsNHS previously approved these changes Feb 13, 2026
Copy link
Collaborator

@DWolfsNHS DWolfsNHS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving for code review, given that this can't merge until the pipeline passes which is pending @neil-sproston implementing a terraform fix.

@sonarqubecloud
Copy link

@github-actions
Copy link

Deployment Complete

Copy link
Contributor

@davidhamill1-nhs davidhamill1-nhs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments.

Comment on lines +6 to +8
STUB_SDS: "1"
STUB_PDS: "1"
STUB_PROVIDER: "1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the variable value meaningful to the context.

Suggested change
STUB_SDS: "1"
STUB_PDS: "1"
STUB_PROVIDER: "1"
STUB_SDS: "true"
STUB_PDS: "true"
STUB_PROVIDER: "true"

Comment on lines +12 to +17
# Access record structured interaction ID from
# https://developer.nhs.uk/apis/gpconnect/accessrecord_structured_development.html#spine-interactions
ACCESS_RECORD_STRUCTURED_INTERACTION_ID = (
"urn:nhs:names:services:gpconnect:fhir:operation:gpc.getstructuredrecord-1"
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this didn't need to go into a common class. It is intrinsically linked to the GetStructuredRecordRequest.

Yes it is going to be use in multiple places, and thus it is a common piece of code, but by placing it as it's own constant in the common module, it has lost its tight coupling with the request to which it is related.

# SDS: Get provider details (ASID + endpoint) for provider ODS
sds = SdsClient(
auth_token=auth_token,
api_key=auth_token,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API key is not the auth token.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think leaving auth journey out the client at the moment makes the most sense. In that way, once we have set up our own mock.

Comment on lines +190 to +205
endpoint_url: str | None = None
if get_endpoint:
endpoint_bundle = self._query_sds(
ods_code=ods_code,
party_key=party_key,
correlation_id=correlation_id,
timeout=timeout,
querytype=self.ENDPOINT,
)
endpoint = self._extract_first_entry(endpoint_bundle)
if endpoint:
address = endpoint.get("address")
if address:
endpoint_url = str(address).strip()

return SdsSearchResults(asid=asid, endpoint=endpoint_url)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use guard clauses to reduce the three layers of nested if statements.

Suggested change
endpoint_url: str | None = None
if get_endpoint:
endpoint_bundle = self._query_sds(
ods_code=ods_code,
party_key=party_key,
correlation_id=correlation_id,
timeout=timeout,
querytype=self.ENDPOINT,
)
endpoint = self._extract_first_entry(endpoint_bundle)
if endpoint:
address = endpoint.get("address")
if address:
endpoint_url = str(address).strip()
return SdsSearchResults(asid=asid, endpoint=endpoint_url)
endpoint_url: str | None = None
if not get_endpoint:
return SdsSearchResults(asid=asid, endpoint=endpoint_url)
endpoint_bundle = self._query_sds(
ods_code=ods_code,
party_key=party_key,
correlation_id=correlation_id,
timeout=timeout,
querytype=self.ENDPOINT,
)
endpoint = self._extract_first_entry(endpoint_bundle)
if endpoint and address in endpoint:
address = endpoint.get("address")
endpoint_url = str(address).strip()
return SdsSearchResults(asid=asid, endpoint=endpoint_url)

Comment on lines +78 to +82
# URLs for different SDS environments
SANDBOX_URL = "https://sandbox.api.service.nhs.uk/spine-directory/FHIR/R4"
INT_URL = "https://int.api.service.nhs.uk/spine-directory/FHIR/R4"
DEP_UAT_URL = "https://dep.api.service.nhs.uk/spine-directory/FHIR/R4"
PROD_URL = "https://api.service.nhs.uk/spine-directory/FHIR/R4"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably the urls will be passed in as config/env variables. We won't ever handle this class attributes.

Comment on lines +22 to +26
assert result is not None
assert isinstance(result, SdsSearchResults)
assert result.asid is not None
assert result.asid == "asid_PROV"
assert len(result.asid) > 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reduce testing implementation we could remove the second assertion.
And the first, third and final assertions are made redundant by the fourth assertion.

Suggested change
assert result is not None
assert isinstance(result, SdsSearchResults)
assert result.asid is not None
assert result.asid == "asid_PROV"
assert len(result.asid) > 0
assert result.asid == "asid_PROV"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find Copilot to be over enthusiastic with assertions.

Comment on lines +10 to +11
class TestSdsIntegration:
"""Integration tests for SDS search operations."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I would class these as integration tests. Maybe I don't see the stubs as a system with which to integrate.

I would read integration tests as requiring a system to be built/stood up with other built/stood-up systems.

Comment on lines +37 to +43
assert result is not None
assert result.asid == "asid_PROV"
assert result.endpoint is not None
assert result.endpoint == "https://provider.example.com/fhir"
# Verify endpoint is a valid URL format
assert result.endpoint.startswith("https://")
assert "fhir" in result.endpoint
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above. This tests also runs the same setup and assertion as the first.

Comment on lines +150 to +154
# Configure external services to use stubs
# To disable, comment these out - if they're set at all, it'll use the stub
ENV STUB_SDS=1
ENV STUB_PDS=1
ENV STUB_PROVIDER=1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these are necessary.

Comment on lines +18 to +20
ENV STUB_SDS=1
ENV STUB_PDS=1
ENV STUB_PROVIDER=1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants